package ru.amse.stroganova.presentation;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Point;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import ru.amse.stroganova.graph.AbstractGraph;
import ru.amse.stroganova.graph.DirectedGraph;
import ru.amse.stroganova.graph.Edge;
import ru.amse.stroganova.graph.UndirectedGraph;
import ru.amse.stroganova.graph.Vertex;

/**
 * This class provides a presentation for any implementation of
 * <tt>AbstractGraph</tt>.
 * 
 * @author Dasha Stroganova
 * 
 */
//TODO think of making it parametrised <E extends AbstractGraph>
public class GraphPresentation {

	private AbstractGraph graph;

	private final Set<VertexPresentation> vertices;

	private final Set<EdgePresentation> edges;

	private final Map<Vertex, VertexPresentation> verticesMap;

	private final Map<Edge, EdgePresentation> edgesMap;

	private final Set<GraphStateListener> listeners;

	private boolean isDirected;

	private boolean isWeighted;

	private int radix;

	//TODO to be eliminated after loading graphs as well as getVertexCenter
	private static final int RADIX_PER_VERTEX = 13;
	
	/**
	 * Shift from (0,0).
	 */
	public static final int SHIFT = 30;

	private final ElementStateListener elementStateListener = new ElementStateListener() {
		public void elementPositionChanged() {
			firePositionChanged();
		}
		public void elementStructureChanged() {
			fireStructureChanged();
		}
	};
	
	private boolean isPaused;
	
	private boolean wasStructureChangedRecieved;
	
	private boolean wasPositionChangedRecieved;
	
	/**
	 * Constructs presentation for the given graph
	 * 
	 * @param graph
	 *            graph
	 * @param isDirected
	 *            true if you want directed graph; false otherwise
	 * @param isWeighted
	 *            true if you want weighted graph; false otherwise
	 */
	public GraphPresentation(AbstractGraph graph, boolean isDirected,
			boolean isWeighted) {
		
		this.graph = graph;
		this.isWeighted = isWeighted;
		this.isDirected = isDirected;
		listeners = new HashSet<GraphStateListener>();
		vertices = new HashSet<VertexPresentation>();
		edges = new HashSet<EdgePresentation>();
		verticesMap = new HashMap<Vertex, VertexPresentation>();
		edgesMap = new HashMap<Edge, EdgePresentation>();
		radix = RADIX_PER_VERTEX * graph.getVertices().size();
		int i = 0;
		for (Vertex vertex : this.graph.getVertices()) {
			VertexPresentation vp = new VertexPresentation(vertex,
					getCenterForVertex(i));
			i++;
			vertices.add(vp);
			verticesMap.put(vertex, vp);
			vp.addElementStateListener(elementStateListener);
			for (Edge edge : vertex.getOutgoingEdges()) {
				if (edgesMap.containsKey(edge)) {
					continue;
				}
				EdgePresentation ep = new EdgePresentation(edge, verticesMap,
						isDirected, isWeighted);
				edges.add(ep);
				edgesMap.put(edge, ep);
				ep.addElementStateListener(elementStateListener);
			}
		}
		isPaused = false;
		wasPositionChangedRecieved = false;
		wasStructureChangedRecieved = false;
	}
	
	/**
	 * Constructs new graph presentation and new graph
	 * 
	 * @param isDirected
	 *            true if you want directed graph; false otherwise
	 * @param isWeighted
	 *            true if you want weighted graph; false otherwise
	 */
	public GraphPresentation(boolean isDirected,
			boolean isWeighted) {
		if (isDirected) {
			graph = new DirectedGraph();
		} else {
			graph = new UndirectedGraph();
		}
		this.isWeighted = isWeighted;
		this.isDirected = isDirected;
		listeners = new HashSet<GraphStateListener>();
		vertices = new HashSet<VertexPresentation>();
		edges = new HashSet<EdgePresentation>();
		verticesMap = new HashMap<Vertex, VertexPresentation>();
		edgesMap = new HashMap<Edge, EdgePresentation>();
	}

	private Point getCenterForVertex(int k) {
		int verticesNumber = graph.getVertices().size();
		int x = (int) Math.round(radix
				* Math.cos(2 * Math.PI * k / verticesNumber));
		int y = (int) Math.round(radix
				* Math.sin(2 * Math.PI * k / verticesNumber));
		return new Point(radix + SHIFT + x, radix + SHIFT + y);
	}

	/**
	 * Adds the specified GraphStateListaner to recive notification about state
	 * change.
	 * 
	 * @param listener
	 */
	public void addGraphStateListener(GraphStateListener listener) {
		listeners.add(listener);
	}
	
	/**
	 * Removes the specified GraphStateListaner from this presentation.
	 * 
	 * @param listener
	 */
	public void removeGraphStateListener(GraphStateListener listener) {
		listeners.remove(listener);
	}
	
	private void firePositionChanged() {
		if (isPaused) {
			wasPositionChangedRecieved = true;
		} else {
			for (GraphStateListener listener : listeners) {
				listener.positionChanged();
			}
		}
	}
	
	private void fireStructureChanged() {
		if (isPaused) {
			wasStructureChangedRecieved = true;
		} else {
			for (GraphStateListener listener : listeners) {
				listener.structureChanged();
			}
		}
	}
	
	/**
	 * Pauses all changes firing until resume is not called.
	 *
	 */
	public void pauseFiringChanges() {
		isPaused = true;
	}
	
	/**
	 * Resumes firing changes.
	 *
	 */
	public void resumeFiringChanges() {
		isPaused = false;
		if (wasPositionChangedRecieved) {
			firePositionChanged();
			wasPositionChangedRecieved = false;
		}
		if (wasStructureChangedRecieved) {
			fireStructureChanged();
			wasStructureChangedRecieved = false;
		}
	}

	/**
	 * Paints this graph using given graphics
	 * 
	 * @param gr
	 *            graphics
	 * @param backgroundColor
	 *            background color
	 */
	public void paint(Graphics gr, Color backgroundColor) {
		for (EdgePresentation ep : edges) {
			ep.paint(gr, backgroundColor);
		}
		for (VertexPresentation vp : vertices) {
			vp.paint(gr);
		}
	}
	
	/**
	 * Adds this vertex presentation to graph presentation.
	 * 
	 * @param vp vertex presentation
	 */
	public void addVertex(VertexPresentation vp) {
		graph.addVertex(vp.getVertex());
		vertices.add(vp);
		verticesMap.put(vp.getVertex(), vp);
		vp.addElementStateListener(elementStateListener);
		fireStructureChanged();
	}
	
	/**
	 * Removes this vertex presentation from graph presentation.
	 * 
	 * @param vp vertex presentation
	 */
	public void removeVertex(VertexPresentation vp) {
		for (Edge edge : vp.getVertex().getOutgoingEdges()) {
			EdgePresentation ep = edgesMap.remove(edge);
			edges.remove(ep);
		}
		for (Edge edge : vp.getVertex().getIncomingEdges()) {
			EdgePresentation ep = edgesMap.remove(edge);
			edges.remove(ep);
		}
		graph.removeVertex(vp.getVertex());
		vertices.remove(vp);
		verticesMap.remove(vp);
		vp.removeElementStateListener(elementStateListener);
		fireStructureChanged();
	}
	
	/**
	 * Adds this edge presentation to graph presentation.
	 * 
	 * @param ep edge presentation
	 */
	public void addEdge(EdgePresentation ep) {
		graph.addEdge(ep.getEdge());
		edges.add(ep);
		edgesMap.put(ep.getEdge(), ep);
		ep.addElementStateListener(elementStateListener);
		fireStructureChanged();
	}
	
	/**
	 * Removes this edge presentation from graph presentation.
	 * 
	 * @param ep edge presentation
	 */
	public void removeEdge(EdgePresentation ep) {
		graph.disconnect(ep.getEdge().getSource(), ep.getEdge()
				.getDestination());
		edges.remove(ep);
		edgesMap.remove(ep);
		ep.removeElementStateListener(elementStateListener);
		fireStructureChanged();
	}

	/**
	 * Checks whether given vertices are connected.
	 * 
	 * @param sourcePr source
	 * @param destinPr destination
	 * @return true if they are; false otherwise
	 */
	public boolean areConnected(VertexPresentation sourcePr, VertexPresentation destinPr) {
		return graph.areConnected(sourcePr.getVertex(), destinPr.getVertex());
	}
	
	/**
	 * Returns preferred graph view size
	 * 
	 * @return size
	 */
	public Dimension getPreferredSize() {
		int resultX = 0;
		int resultY = 0;
		for (VertexPresentation vp : vertices) {
			resultX = Math.max(resultX, vp.getCenter().x + VertexPresentation.RADIX + 1);
			resultY = Math.max(resultY, vp.getCenter().y + VertexPresentation.RADIX + 1);
		}
		return new Dimension(resultX, resultY);
	}

	/**
	 * Returns model graph of this presentation.
	 * 
	 * @return graph
	 */
	public AbstractGraph getGraph() {
		return graph;
	}
	
	/**
	 * Returns set of vertices presentations.
	 * 
	 * @return set of vertices presentations
	 */
	public Set<VertexPresentation> getVertexPresentations() {
		return vertices;
	}
	
	/**
	 * Returns set of edges presentations.
	 * 
	 * @return set of edges presentations
	 */
	public Set<EdgePresentation> getEdgePresentations() {
		return edges;
	}
	
	/**
	 * Returns presentations of edges incident to given vertex presentation.
	 * 
	 * @param vp vertex presentation
	 * @return edges incident to given vertex
	 */
	public Set<EdgePresentation> getIncidendEdgePresentations(VertexPresentation vp) {
		Set<EdgePresentation> result = new HashSet<EdgePresentation>();
		for (Edge edge : vp.getVertex().getOutgoingEdges()) {
			result.add(edgesMap.get(edge));
		}
		for (Edge edge : vp.getVertex().getIncomingEdges()) {
			result.add(edgesMap.get(edge));
		}
		return result;
	}
	
	/**
	 * Checks whether this presentation is weighted.
	 * 
	 * @return true if it is; false otherwise
	 */
	public boolean isWeighted() {
		return isWeighted;
	}
	
	/**
	 * Checks whether this presentation is directed.
	 * 
	 * @return true if it is; false otherwise
	 */
	public boolean isDirected() {
		return isDirected;
	}
	
	/**
	 * Returns edge presentation for given edge.
	 * 
	 * @param edge edge
	 * @return edge presentation
	 */
	public EdgePresentation getPresentationForEdge(Edge edge) {
		return edgesMap.get(edge);
	}
	
	/**
	 * Returns vertex presentation for given vertex.
	 * 
	 * @param vertex vertex
	 * @return vertex presentation
	 */
	public VertexPresentation getPresentationForVertex(Vertex vertex) {
		return verticesMap.get(vertex);
	}
}
